feat: implement client template management system #286
feat: implement client template management system #286ImMohammad20000 wants to merge 12 commits intodevfrom
Conversation
- Removed legacy XRAY subscription template file and related configurations. - Introduced a new CoreTemplate model with CRUD operations for managing core templates. - Added API endpoints for creating, retrieving, modifying, and deleting core templates. - Implemented validation for template content based on type (e.g., clash, xray, singbox). - Created default templates for clash, xray, singbox, user-agent, and gRPC user-agent. - Added unit tests for core template creation, retrieval, and deletion logic.
WalkthroughThis PR introduces a database-driven Core Templates system that moves subscription templates (Clash, Xray, SingBox) and user-agent configurations from static files to persistent database storage. It includes CRUD operations, API endpoints with admin access control, database migration, and cache synchronization via a message bus. Changes
Sequence DiagramsequenceDiagram
participant Admin as Admin User
participant API as API Router
participant Op as CoreTemplateOperation
participant DB as Database
participant Cache as Cache Layer
participant Bus as Message Bus
participant Sub as Subscription Generator
Admin->>API: PUT /api/core_template/{id}
API->>Op: modify_core_template(db, id, changes, admin)
Op->>DB: Update template content
Op->>Cache: _sync_core_template_cache()
Cache->>Bus: Publish CORE_TEMPLATE message
Bus->>Cache: handle_core_template_message()
Cache->>Cache: refresh_core_templates_cache()
Note over Sub: Next subscription request
Sub->>Cache: subscription_core_templates()
Cache-->>Sub: Return updated templates
Sub->>Sub: Build configs with new content
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/subscription/singbox.py (1)
56-72:⚠️ Potential issue | 🔴 CriticalPipeline failure:
KeyError: 'type'when outbound entries lack a"type"field.Now that templates are user-configurable,
outbound["type"]on Lines 58/60 will raiseKeyErrorif any outbound in the loaded config is missing the"type"key. Previously, the static template guaranteed this field. Use.get("type")to guard against malformed entries.🐛 Proposed fix
def render(self, reverse=False): urltest_types = ["vmess", "vless", "trojan", "shadowsocks", "hysteria2", "tuic", "http", "ssh"] - urltest_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound["type"] in urltest_types] + urltest_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound.get("type") in urltest_types] selector_types = ["vmess", "vless", "trojan", "shadowsocks", "hysteria2", "tuic", "http", "ssh", "urltest"] - selector_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound["type"] in selector_types] + selector_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound.get("type") in selector_types]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/singbox.py` around lines 56 - 72, In render(), the list comprehensions building urltest_tags and selector_tags access outbound["type"] which raises KeyError for outbounds missing the "type" key; change those to use outbound.get("type") (e.g., in the comprehensions that create urltest_tags and selector_tags) so malformed entries are skipped safely and behavior remains the same; adjust any similar direct dict indexing in render() to .get("type") to prevent the crash.
🧹 Nitpick comments (21)
app/subscription/core_templates.py (1)
7-10: Consider adding a TTL to@cached()as a safety net against stale data.
@cached()with nottlargument means the cached value never expires — invalidation relies entirely on NATS messages reachinghandle_core_template_message. If NATS delivery fails or is delayed, stale template data persists indefinitely.Adding a modest TTL (e.g., 5–10 minutes) provides a safety net:
♻️ Suggested change
-@cached() +@cached(ttl=300) async def subscription_core_templates() -> dict[str, str]:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/core_templates.py` around lines 7 - 10, The cached subscription_core_templates function currently uses `@cached`() with no ttl so values never expire; add a modest TTL (e.g., ttl=300 or ttl=600) to the `@cached` decorator on subscription_core_templates to ensure the cache auto-invalidates periodically as a safety net if NATS messages to handle_core_template_message are missed or delayed, while keeping get_core_template_values and the existing manual invalidation logic intact (use the same decorator signature where subscription_core_templates is defined).app/subscription/default_templates.py (1)
18-22: Hardcoded region-specific DNS servers in default template.The
proxy-server-nameserverentries (178.22.122.100,78.157.42.100) are Shecan DNS servers specific to Iran. If this panel is intended for broader use, consider using more universally appropriate defaults (e.g.,1.1.1.1,8.8.8.8) or documenting these as region-specific.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/default_templates.py` around lines 18 - 22, The default template contains hardcoded region-specific DNS entries under the key "proxy-server-nameserver" (values 178.22.122.100 and 78.157.42.100); update the template in app/subscription/default_templates.py to use globally appropriate defaults (for example 1.1.1.1 and 8.8.8.8) or replace these values with clearly documented region-specific placeholders and add a comment/README note indicating they are Iran-specific so operators can override them; locate the "nameserver" and "proxy-server-nameserver" entries in the template and change the values or add the documentation accordingly.app/db/models.py (1)
717-729: Model looks well-structured.The composite unique constraint and type index align with the query patterns in the CRUD layer. Using
String(32)fortemplate_type(instead ofSQLEnum) defers validation to the application layer, which is flexible.One optional observation: unlike most other models in this file (
Admin,Node,CoreConfig, etc.),CoreTemplatelacks acreated_attimestamp. If auditing template creation/modification times is useful, consider adding one.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/models.py` around lines 717 - 729, CoreTemplate is missing a created_at timestamp—add a Mapped[datetime] mapped_column named created_at to the CoreTemplate model (class CoreTemplate) so creation time is recorded; implement it with a non-nullable DateTime column using a server default (e.g., func.now()) to auto-populate, and ensure you import DateTime and func from sqlalchemy if not already present.app/db/crud/core_template.py (6)
134-141:isinstance(s.value, tuple)branch is dead code.
CoreTemplateSortingOptionsSimplevalues are SQLAlchemyUnaryExpressionobjects (produced by.asc()/.desc()), never tuples. Thesort_list.extend(s.value)path is unreachable and can be simplified away.♻️ Proposed simplification
if sort: - sort_list = [] - for s in sort: - if isinstance(s.value, tuple): - sort_list.extend(s.value) - else: - sort_list.append(s.value) - stmt = stmt.order_by(*sort_list) + stmt = stmt.order_by(*[s.value for s in sort])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 134 - 141, The tuple branch is dead because CoreTemplateSortingOptionsSimple items yield SQLAlchemy UnaryExpression objects, not tuples; update the sorting logic in core_template.py to always collect s.value (e.g., replace the isinstance branch with a simple comprehension or single append per item) so sort_list becomes [s.value for s in sort] and then call stmt = stmt.order_by(*sort_list); reference symbols: sort, sort_list, s.value, and stmt.order_by.
134-141: Dead branch:isinstance(s.value, tuple)is neverTrue.
CoreTemplateSortingOptionsSimplevalues are SQLAlchemyUnaryExpressionobjects (produced by.asc()/.desc()), never tuples. Thesort_list.extend(s.value)path is unreachable and can be removed.♻️ Proposed simplification
if sort: - sort_list = [] - for s in sort: - if isinstance(s.value, tuple): - sort_list.extend(s.value) - else: - sort_list.append(s.value) - stmt = stmt.order_by(*sort_list) + stmt = stmt.order_by(*[s.value for s in sort])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 134 - 141, The branch checking isinstance(s.value, tuple) is dead because CoreTemplateSortingOptionsSimple produces SQLAlchemy UnaryExpression objects (from .asc()/.desc()), so remove the unreachable tuple branch and simplify the loop in the sort handling: build sort_list by appending s.value for each s in sort and then call stmt.order_by(*sort_list); update the block around sort, sort_list, s.value, and stmt.order_by accordingly to eliminate the isinstance(...) branch.
62-64: Silent DB failure inget_core_template_valuesemits no log.When the
SELECTfails withSQLAlchemyError, the function silently falls back to hardcoded defaults with no warning. Operators would have no visibility that the database is unreachable and all subscribers are being served stale defaults.♻️ Proposed fix — add a warning log
except SQLAlchemyError: + logger.warning("Failed to fetch core template values from DB; falling back to defaults", exc_info=True) return defaults(Import or reuse a module-level logger, e.g.
logger = get_logger("core-template-crud"))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 62 - 64, The catch in get_core_template_values currently swallows SQLAlchemyError and returns defaults silently; update the function to log a warning with the exception details before returning defaults by obtaining/using a module-level logger (e.g. define logger = get_logger("core-template-crud") at top or reuse an existing logger) and change the except block to log the error/stack (include the exception object) and then return defaults so operators can see DB failures.
108-111:if limit:skips the limit clause whenlimit=0, returning all rows instead of none.Using truthiness on an
intparameter treats0as falsy. Foroffset=0this is a no-op (harmless), butlimit=0silently drops the.limit(0)call, returning the full result set. The same pattern appears inget_core_templates_simple(lines 148–151).♻️ Proposed fix (apply identically in
get_core_templates_simplelines 148–151)- if offset: + if offset is not None: query = query.offset(offset) - if limit: + if limit is not None: query = query.limit(limit)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 108 - 111, The conditionals use truthiness so limit=0 is treated as False and query.limit(0) is skipped; change the checks to explicit None checks (e.g., replace "if limit:" with "if limit is not None:" and similarly "if offset:" with "if offset is not None:") in the function that builds the query and also apply the identical fix inside get_core_templates_simple so query.limit(0) and query.offset(0) are honored.
62-64: Silent DB failure inget_core_template_values— no log emitted.When the SELECT fails with
SQLAlchemyErrorthe function silently returns hardcoded defaults. Operators would have no indication that the DB is unreachable and templates have fallen back to defaults. A warning-level log before thereturn defaultswould make this observable.♻️ Proposed fix
+import logging +_logger = logging.getLogger(__name__) ... except SQLAlchemyError: + _logger.warning("Failed to fetch core template values from DB; falling back to defaults", exc_info=True) return defaults🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 62 - 64, get_core_template_values currently swallows SQLAlchemyError and returns defaults silently; update the except block to log a warning before returning. Specifically, in get_core_template_values catch SQLAlchemyError as e (or similar), call the module logger (e.g., logger.warning or processLogger.warning) with a clear message including the exception details and context (e.g., "Failed to fetch core template values, falling back to defaults: %s" % e), then return defaults. Ensure you reference SQLAlchemyError, the defaults variable, and the get_core_template_values function when making the change.
108-111: Useis not Noneguard foroffsetandlimitto avoid incorrect behavior withlimit=0.
if offset:andif limit:treat0as falsy. Foroffset=0this is harmless (no-op), butlimit=0silently skips the limit and returns all rows instead of zero. The same pattern repeats inget_core_templates_simple(lines 148–151).♻️ Proposed fix (apply the same change in `get_core_templates_simple` lines 148–151)
- if offset: + if offset is not None: query = query.offset(offset) - if limit: + if limit is not None: query = query.limit(limit)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/db/crud/core_template.py` around lines 108 - 111, Change the truthy checks for offset and limit to explicit None checks so zero values aren't treated as falsy: replace the "if offset:" and "if limit:" guards with "if offset is not None:" and "if limit is not None:" in the function containing the shown query modifications (and apply the same change in get_core_templates_simple where the same pattern occurs) so a provided limit=0 correctly applies a zero-row limit.app/models/core_template.py (2)
76-81: Inconsistent total-count field names across list response types.
CoreTemplateResponseList(line 77) calls the total countcount, whileCoreTemplatesSimpleResponse(line 94) calls ittotal. API consumers that handle both shapes will need special-case knowledge of which field to read.♻️ Proposed alignment (unify to `total`)
class CoreTemplateResponseList(BaseModel): - count: int + total: int templates: list[CoreTemplateResponse] = []Update the construction in
app/operation/core_template.pyline 95:- return CoreTemplateResponseList(templates=templates, count=count) + return CoreTemplateResponseList(templates=templates, total=count)Also applies to: 92-94
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/models/core_template.py` around lines 76 - 81, The list response uses inconsistent total field names: change CoreTemplateResponseList.count to CoreTemplateResponseList.total (type int) and update any construction/usages that build instances (e.g., the response created in the code that constructs CoreTemplateResponseList in the core template operations—previously setting "count") to set "total" instead; ensure CoreTemplatesSimpleResponse and CoreTemplateResponseList now share the same field name and adjust any tests/serializations that read "count" to read "total" (leave model_config as-is).
76-81: Inconsistent list-response field naming:countvstotal.
CoreTemplateResponseListexposes the size field ascount(line 77), whileCoreTemplatesSimpleResponsecalls the same concepttotal(line 94). API consumers that handle both response shapes will need to know which field name to use, which is error-prone.♻️ Proposed alignment (pick one name consistently)
class CoreTemplateResponseList(BaseModel): - count: int + total: int templates: list[CoreTemplateResponse] = [] model_config = ConfigDict(from_attributes=True)Then update the construction site in
app/operation/core_template.pyline 95:- return CoreTemplateResponseList(templates=templates, count=count) + return CoreTemplateResponseList(templates=templates, total=count)Also applies to: 92-94
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/models/core_template.py` around lines 76 - 81, The list response fields are inconsistent: CoreTemplateResponseList defines the size field as count while CoreTemplatesSimpleResponse uses total; pick one name (e.g., total) and make both models use it consistently by renaming the field in CoreTemplateResponseList (replace count:int with total:int) and adjusting any default/initialization (templates: list[CoreTemplateResponse] = []). Then update the code that constructs these DTOs (the place that creates a CoreTemplateResponseList instance in the core_template operation) to pass/consume the chosen field name (total) instead of count so both response shapes match.tests/api/test_core_template.py (1)
7-26: Tests don't clean up created templates.None of the test functions delete the templates they create. If the test database isn't reset between runs (or between tests in the same session), leftover templates could cause flaky results in other tests. Consider adding cleanup in a
finallyblock or using a pytest fixture with teardown.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/api/test_core_template.py` around lines 7 - 26, The test_core_template_create_and_get test creates a template via create_core_template but never deletes it, risking test pollution; modify the test to remove the created template in a teardown step (e.g., wrap the create/assert logic in try/finally and call the API to delete the template by created["id"] in the finally block) or move creation to a pytest fixture that yields the created template and deletes it in the fixture teardown, ensuring cleanup uses the same client/Authorization header used in client.get.app/routers/core_template.py (3)
66-74: Unconventional route construction using"s"suffix.The list endpoints are defined as
@router.get("s")and@router.get("s/simple"), relying on the prefix/api/core_templateto form/api/core_templatesand/api/core_templates/simple. While functional, this is non-obvious and could confuse contributors. A more conventional approach would be to define the prefix as/api/core_templatesand use""/"/simple"for list routes, and"/{template_id}"for singular resource routes — or split into two routers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/routers/core_template.py` around lines 66 - 74, The route decorators use an unconventional `"s"` suffix to build plural paths (e.g., `@router.get`("s") on get_core_templates) which is confusing; update the router prefix to a plural form (e.g., change the router prefix from "/api/core_template" to "/api/core_templates") and then change the list route decorators to use "" and "/simple" (e.g., `@router.get`("") for get_core_templates and `@router.get`("/simple") for get_core_templates_simple) and ensure singular routes use "/{template_id}" (e.g., get_core_template_by_id) so path construction is explicit and readable.
84-84: Parameterallshadows the Python built-in.Renaming to e.g.
fetch_allorinclude_allavoids shadowing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/routers/core_template.py` at line 84, The parameter named "all" shadows the Python built-in; rename it (e.g., to include_all or fetch_all) in the function signature where "all: bool = False" appears (and update the parameter annotation) and then update every usage inside that function and all call sites to use the new name (also adjust any type hints, default handling, and tests or docs referencing the old name). Ensure the new identifier is used consistently in any related methods or templates that forwarded the parameter (search for "all=" and "all :" occurrences) to avoid runtime NameError or shadowing issues.
56-63: Returning{}from a 204 No Content endpoint.HTTP 204 semantics require no response body. Returning an empty dict is not idiomatic and may cause some clients or middleware to behave unexpectedly. Return
Noneor useResponse(status_code=204)instead.♻️ Suggested fix
`@router.delete`("/{template_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_core_template( template_id: int, db: AsyncSession = Depends(get_db), admin: AdminDetails = Depends(check_sudo_admin), ): await core_template_operator.remove_core_template(db, template_id, admin) - return {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/routers/core_template.py` around lines 56 - 63, The delete endpoint remove_core_template currently returns an empty dict which violates 204 No Content semantics; update the function (remove_core_template in app/routers/core_template.py) to not return a body — either return None or explicitly return a fastapi.Response(status_code=status.HTTP_204_NO_CONTENT) after calling core_template_operator.remove_core_template(db, template_id, admin) so no response body is emitted.app/subscription/clash.py (1)
31-31: Inconsistent None-check pattern:orvsis None.
base.pyandsingbox.pyuseif xxx is None:to default template content, but hereoris used. Withor, an empty string""would also trigger the fallback (which may be desirable), but it's inconsistent with the sibling classes. Consider aligning to one pattern across all subscription classes for predictable behavior.♻️ Suggested alignment with other classes
- self.clash_template_content = clash_template_content or DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE + if clash_template_content is None: + clash_template_content = DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE + self.clash_template_content = clash_template_content🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/clash.py` at line 31, The current assignment uses "self.clash_template_content = clash_template_content or DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE", which treats empty strings as missing and differs from the "if ... is None:" pattern used in base.py and singbox.py; change the logic to mirror those classes by checking "if clash_template_content is None:" and only then set self.clash_template_content = DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE (otherwise assign the provided value) so empty-string inputs remain preserved and behavior is consistent with other subscription classes.app/subscription/base.py (2)
37-37: Unnecessary explicitdelof local variables.
user_agent_dataandgrpc_user_agent_dataare local variables that will be garbage-collected when__init__returns. The explicitdeladds no value.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/base.py` at line 37, Remove the unnecessary explicit deletion of local variables: delete the statement "del user_agent_data, grpc_user_agent_data" from the __init__ method so that local variables user_agent_data and grpc_user_agent_data are allowed to go out of scope naturally and be garbage-collected without an explicit del.
24-30: No error handling around template rendering/parsing.If a core template stored in the DB contains invalid Jinja syntax or malformed JSON,
render_template_stringorjson.loadswill raise an unhandled exception, breaking subscription generation for all users. While validation occurs at template creation time, consider wrapping these in a try/except that falls back to the default template and logs a warning, so a corrupt template doesn't take down the subscription endpoint.🛡️ Suggested defensive fallback
+ import logging + logger = logging.getLogger(__name__) + self.proxy_remarks = [] - user_agent_data = json.loads(render_template_string(user_agent_template_content)) + try: + user_agent_data = json.loads(render_template_string(user_agent_template_content)) + except Exception: + logger.warning("Failed to render user-agent template, falling back to default") + user_agent_data = json.loads(render_template_string(DEFAULT_USER_AGENT_TEMPLATE)) if "list" in user_agent_data and isinstance(user_agent_data["list"], list): self.user_agent_list = user_agent_data["list"] else: self.user_agent_list = [] - grpc_user_agent_data = json.loads(render_template_string(grpc_user_agent_template_content)) + try: + grpc_user_agent_data = json.loads(render_template_string(grpc_user_agent_template_content)) + except Exception: + logger.warning("Failed to render gRPC user-agent template, falling back to default") + grpc_user_agent_data = json.loads(render_template_string(DEFAULT_GRPC_USER_AGENT_TEMPLATE))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/base.py` around lines 24 - 30, The template rendering and JSON parsing calls (render_template_string + json.loads) for user_agent_template_content and grpc_user_agent_template_content can raise and crash subscription generation; wrap each render/json.loads block in a try/except that catches Exception, logs a warning via the class logger (e.g., self.logger.warning or whatever logger is used in this class) including which template failed and the exception, and fall back to safe defaults (set self.user_agent_list = [] and grpc_user_agent_data = {} or a default template result) so a corrupt template doesn't break the subscription endpoint; apply this defensive pattern around both the user_agent and grpc_user_agent processing in the same method.app/subscription/share.py (2)
75-76: No error handling ifsubscription_core_templates()fails.If the DB/cache call fails, the exception will propagate directly to the HTTP handler. Since this is a user-facing subscription endpoint, consider a try/except with a fallback to default templates so that subscription generation remains available even during transient DB issues.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/share.py` around lines 75 - 76, Wrap the call to subscription_core_templates() in a try/except to catch any exceptions, log the error (e.g., logging.exception or processLogger.error) and set a safe fallback for core_templates (for example a default templates list or an empty set) so that _build_subscription_config(config_format, core_templates) still runs and returns a usable conf; ensure you reference subscription_core_templates(), core_templates and _build_subscription_config in the change and do not let the original exception propagate to the HTTP handler.
37-65: Hard-coded dict keys are fragile — consider using constants.The template keys (
"USER_AGENT_TEMPLATE","GRPC_USER_AGENT_TEMPLATE","XRAY_SUBSCRIPTION_TEMPLATE","SINGBOX_SUBSCRIPTION_TEMPLATE","CLASH_SUBSCRIPTION_TEMPLATE") are raw string literals. A typo in any of them will produce aKeyErrorat runtime with no compile-time safety. Define these as module-level constants (or reference an enum) so they're defined once and shared withsubscription_core_templates().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/share.py` around lines 37 - 65, Replace the raw string template keys in _build_subscription_config with module-level constants (e.g., USER_AGENT_TEMPLATE, GRPC_USER_AGENT_TEMPLATE, XRAY_SUBSCRIPTION_TEMPLATE, SINGBOX_SUBSCRIPTION_TEMPLATE, CLASH_SUBSCRIPTION_TEMPLATE) and use those constants when constructing common_kwargs and when passing template_*_content into StandardLinks, ClashMetaConfiguration, SingBoxConfiguration, XrayConfiguration, and OutlineConfiguration; also update the provider of core_templates (subscription_core_templates) to populate and export the same constants so both functions reference the single source of truth and avoid KeyError from mistyped literal keys.app/subscription/singbox.py (1)
19-31: Same lack of error handling asbase.pyfor template rendering.
json.loads(render_template_string(singbox_template_content))on Line 31 will raise an unhandled exception if the template content contains invalid Jinja or malformed JSON. Consider wrapping with a try/except that falls back toDEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE, consistent with the recommendation forbase.py.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/subscription/singbox.py` around lines 19 - 31, Wrap the template rendering and JSON parsing in SingboxSubscription.__init__ in a try/except around the call to json.loads(render_template_string(singbox_template_content)) so template/Jinja/JSON errors are caught; on exception, fall back to parsing DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and assign to self.config (and optionally log the error), keeping render_template_string, json.loads, DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and self.config as the referenced symbols to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/db/migrations/versions/e8c6a4f1d2b7_add_core_templates_table.py`:
- Line 350: The gRPC User-Agent template string in the migration
e8c6a4f1d2b7_add_core_templates_table.py contains a stray ']' character inside
the JSON/seed value ("grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)]"); remove
the trailing ']' so the seeded string becomes "grpc-ruby/1.62.0 grpc-c/39.0.0
(osx; chttp2)" and update the value where it is assigned (the core
templates/default templates list in that migration) so the database seeds with
the corrected User-Agent.
- Line 350: The seeded User-Agent string in the migration
e8c6a4f1d2b7_add_core_templates_table contains a stray trailing bracket — the
value `"grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)]"` — which corrupts
persisted data; remove the trailing `]` so the seeded value becomes
`"grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)"` in the migration’s INSERT/seed
payload (leave all surrounding JSON formatting intact) and re-run the
migration/seed so the corrected string is stored.
In `@app/operation/core_template.py`:
- Around line 172-179: The deletion of a core template is being committed inside
remove_core_template while the default promotion happens afterwards in
set_default_template, which can leave the type without a default on failure;
change the flow so both delete and promotion occur in a single DB transaction:
either modify remove_core_template to only call db.delete() without committing
and let the caller (the code around get_first_template_by_type /
set_default_template) perform the transaction/commit, or implement a new atomic
helper (e.g., remove_core_template_and_promote_default) that runs delete +
set_default_template logic inside one transactional unit and commits once both
succeed; update callers to use the new transactional behavior and ensure no
intermediate commit happens in remove_core_template.
- Around line 172-179: The deletion commits separately causing a window where a
failure in promotion leaves the type without a default; change the flow so
deletion and promotion occur in one transaction: update remove_core_template to
only call db.delete(db_template) (no db.commit()) or add a non-committing
variant, then in the caller (the function handling db_template removal) call
get_first_template_by_type (if db_template.is_default) and set_default_template
as needed, and finally perform a single db.commit() once both deletion and
default promotion succeed; references: remove_core_template,
set_default_template, get_first_template_by_type, db_template, replacement.
- Around line 46-66: The JSON validation branch in _validate_template_content
calls render_template_string(content) with no context so undefined Jinja
variables render to empty strings and json.loads can fail; update the JSON
branch in _validate_template_content to call render_template_string with a
minimal stub context matching the JSON template kinds (e.g., provide keys like
outbounds: [], inbounds: [], proxies: [], proxy-groups: [], rules: [],
proxy_remarks: [] as appropriate) before json.loads, or alternatively skip
rendering and validate raw JSON structure first; ensure changes reference
CoreTemplateType and keep the existing parsed-type check (e.g., for
CoreTemplateType.user_agent and CoreTemplateType.grpc_user_agent) so templates
still must render to a JSON object.
- Around line 46-66: The JSON template validation in _validate_template_content
is inconsistent: only CoreTemplateType.clash_subscription gets a stub Jinja2
context before rendering, causing other types (e.g., Xray, SingBox, user_agent,
grpc_user_agent) to fail on templates that reference variables; update the
render call(s) inside _validate_template_content so all template types use an
appropriate mock context (e.g., the same {"conf": {"proxies": [],
"proxy-groups": [], "rules": []}, "proxy_remarks": []} or a minimal common stub)
when calling render_template_string for non-clash types, then parse the rendered
output (yaml.safe_load or json.loads as appropriate) and keep the existing
exception handling and raise_error behavior.
In `@app/subscription/share.py`:
- Around line 58-59: OutlineConfiguration currently subclasses BaseSubscription
but its __init__ does not call super().__init__, leaving the parent
uninitialized; either remove the inheritance or update
OutlineConfiguration.__init__ to call
super().__init__(user_agent_template_content, grpc_user_agent_template_content)
and properly store/handle those template args, and update the instantiation site
(the branch that returns OutlineConfiguration()) to pass the needed template
content variables so BaseSubscription is initialized correctly; refer to the
OutlineConfiguration class and BaseSubscription.__init__ for where to add the
super call and to the place that constructs OutlineConfiguration to supply the
two template parameters.
In `@app/templates/__init__.py`:
- Around line 24-25: The render_template_string function currently calls
env.from_string(template_content) which renders untrusted DB-sourced templates
with an unsandboxed Jinja2 Environment; replace this with a
SandboxedEnvironment-based render: create or use
jinja2.sandbox.SandboxedEnvironment to parse and render template_content
(preserving the same context fallback logic), so that render_template_string
uses SandboxedEnvironment.from_string(...).render(context or {}) instead of
env.from_string(...).render(...), ensuring the template execution is sandboxed
for defense-in-depth while keeping the function signature and context handling
intact.
In `@tests/api/test_core_template.py`:
- Around line 29-54: The test test_core_template_can_switch_default is brittle
because DB migrations may seed a default template for template_type
"singbox_subscription"; instead first query any existing default for that type
(e.g., GET
/api/core_template?template_type=singbox_subscription&is_default=true) or create
a template and explicitly set it as default via create_core_template to
guarantee a known previous default, then create the second template with
is_default=True using create_core_template and finally fetch both templates
(using client.get on the IDs created) to assert the original default is now
is_default False and the new one is True; update references to
create_core_template, test_core_template_can_switch_default, and the client.get
checks accordingly.
- Around line 57-88: The test test_core_template_last_and_system_delete_guards
assumes no seeded templates for template_type "grpc_user_agent"; update the test
to avoid seeded types by using a unique/non-seeded template_type (e.g., change
template_type passed to create_core_template in that test) or first query
existing templates for that type and adjust the expected responses accordingly
so the delete-guard assertions reflect whether a system/last template already
exists; modify references in the test function
(test_core_template_last_and_system_delete_guards and the two
create_core_template calls) to use the new non-seeded type or include a
pre-check that accounts for seeded data before asserting 403/204.
---
Outside diff comments:
In `@app/subscription/singbox.py`:
- Around line 56-72: In render(), the list comprehensions building urltest_tags
and selector_tags access outbound["type"] which raises KeyError for outbounds
missing the "type" key; change those to use outbound.get("type") (e.g., in the
comprehensions that create urltest_tags and selector_tags) so malformed entries
are skipped safely and behavior remains the same; adjust any similar direct dict
indexing in render() to .get("type") to prevent the crash.
---
Nitpick comments:
In `@app/db/crud/core_template.py`:
- Around line 134-141: The tuple branch is dead because
CoreTemplateSortingOptionsSimple items yield SQLAlchemy UnaryExpression objects,
not tuples; update the sorting logic in core_template.py to always collect
s.value (e.g., replace the isinstance branch with a simple comprehension or
single append per item) so sort_list becomes [s.value for s in sort] and then
call stmt = stmt.order_by(*sort_list); reference symbols: sort, sort_list,
s.value, and stmt.order_by.
- Around line 134-141: The branch checking isinstance(s.value, tuple) is dead
because CoreTemplateSortingOptionsSimple produces SQLAlchemy UnaryExpression
objects (from .asc()/.desc()), so remove the unreachable tuple branch and
simplify the loop in the sort handling: build sort_list by appending s.value for
each s in sort and then call stmt.order_by(*sort_list); update the block around
sort, sort_list, s.value, and stmt.order_by accordingly to eliminate the
isinstance(...) branch.
- Around line 62-64: The catch in get_core_template_values currently swallows
SQLAlchemyError and returns defaults silently; update the function to log a
warning with the exception details before returning defaults by obtaining/using
a module-level logger (e.g. define logger = get_logger("core-template-crud") at
top or reuse an existing logger) and change the except block to log the
error/stack (include the exception object) and then return defaults so operators
can see DB failures.
- Around line 108-111: The conditionals use truthiness so limit=0 is treated as
False and query.limit(0) is skipped; change the checks to explicit None checks
(e.g., replace "if limit:" with "if limit is not None:" and similarly "if
offset:" with "if offset is not None:") in the function that builds the query
and also apply the identical fix inside get_core_templates_simple so
query.limit(0) and query.offset(0) are honored.
- Around line 62-64: get_core_template_values currently swallows SQLAlchemyError
and returns defaults silently; update the except block to log a warning before
returning. Specifically, in get_core_template_values catch SQLAlchemyError as e
(or similar), call the module logger (e.g., logger.warning or
processLogger.warning) with a clear message including the exception details and
context (e.g., "Failed to fetch core template values, falling back to defaults:
%s" % e), then return defaults. Ensure you reference SQLAlchemyError, the
defaults variable, and the get_core_template_values function when making the
change.
- Around line 108-111: Change the truthy checks for offset and limit to explicit
None checks so zero values aren't treated as falsy: replace the "if offset:" and
"if limit:" guards with "if offset is not None:" and "if limit is not None:" in
the function containing the shown query modifications (and apply the same change
in get_core_templates_simple where the same pattern occurs) so a provided
limit=0 correctly applies a zero-row limit.
In `@app/db/models.py`:
- Around line 717-729: CoreTemplate is missing a created_at timestamp—add a
Mapped[datetime] mapped_column named created_at to the CoreTemplate model (class
CoreTemplate) so creation time is recorded; implement it with a non-nullable
DateTime column using a server default (e.g., func.now()) to auto-populate, and
ensure you import DateTime and func from sqlalchemy if not already present.
In `@app/models/core_template.py`:
- Around line 76-81: The list response uses inconsistent total field names:
change CoreTemplateResponseList.count to CoreTemplateResponseList.total (type
int) and update any construction/usages that build instances (e.g., the response
created in the code that constructs CoreTemplateResponseList in the core
template operations—previously setting "count") to set "total" instead; ensure
CoreTemplatesSimpleResponse and CoreTemplateResponseList now share the same
field name and adjust any tests/serializations that read "count" to read "total"
(leave model_config as-is).
- Around line 76-81: The list response fields are inconsistent:
CoreTemplateResponseList defines the size field as count while
CoreTemplatesSimpleResponse uses total; pick one name (e.g., total) and make
both models use it consistently by renaming the field in
CoreTemplateResponseList (replace count:int with total:int) and adjusting any
default/initialization (templates: list[CoreTemplateResponse] = []). Then update
the code that constructs these DTOs (the place that creates a
CoreTemplateResponseList instance in the core_template operation) to
pass/consume the chosen field name (total) instead of count so both response
shapes match.
In `@app/routers/core_template.py`:
- Around line 66-74: The route decorators use an unconventional `"s"` suffix to
build plural paths (e.g., `@router.get`("s") on get_core_templates) which is
confusing; update the router prefix to a plural form (e.g., change the router
prefix from "/api/core_template" to "/api/core_templates") and then change the
list route decorators to use "" and "/simple" (e.g., `@router.get`("") for
get_core_templates and `@router.get`("/simple") for get_core_templates_simple) and
ensure singular routes use "/{template_id}" (e.g., get_core_template_by_id) so
path construction is explicit and readable.
- Line 84: The parameter named "all" shadows the Python built-in; rename it
(e.g., to include_all or fetch_all) in the function signature where "all: bool =
False" appears (and update the parameter annotation) and then update every usage
inside that function and all call sites to use the new name (also adjust any
type hints, default handling, and tests or docs referencing the old name).
Ensure the new identifier is used consistently in any related methods or
templates that forwarded the parameter (search for "all=" and "all :"
occurrences) to avoid runtime NameError or shadowing issues.
- Around line 56-63: The delete endpoint remove_core_template currently returns
an empty dict which violates 204 No Content semantics; update the function
(remove_core_template in app/routers/core_template.py) to not return a body —
either return None or explicitly return a
fastapi.Response(status_code=status.HTTP_204_NO_CONTENT) after calling
core_template_operator.remove_core_template(db, template_id, admin) so no
response body is emitted.
In `@app/subscription/base.py`:
- Line 37: Remove the unnecessary explicit deletion of local variables: delete
the statement "del user_agent_data, grpc_user_agent_data" from the __init__
method so that local variables user_agent_data and grpc_user_agent_data are
allowed to go out of scope naturally and be garbage-collected without an
explicit del.
- Around line 24-30: The template rendering and JSON parsing calls
(render_template_string + json.loads) for user_agent_template_content and
grpc_user_agent_template_content can raise and crash subscription generation;
wrap each render/json.loads block in a try/except that catches Exception, logs a
warning via the class logger (e.g., self.logger.warning or whatever logger is
used in this class) including which template failed and the exception, and fall
back to safe defaults (set self.user_agent_list = [] and grpc_user_agent_data =
{} or a default template result) so a corrupt template doesn't break the
subscription endpoint; apply this defensive pattern around both the user_agent
and grpc_user_agent processing in the same method.
In `@app/subscription/clash.py`:
- Line 31: The current assignment uses "self.clash_template_content =
clash_template_content or DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE", which treats
empty strings as missing and differs from the "if ... is None:" pattern used in
base.py and singbox.py; change the logic to mirror those classes by checking "if
clash_template_content is None:" and only then set self.clash_template_content =
DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE (otherwise assign the provided value) so
empty-string inputs remain preserved and behavior is consistent with other
subscription classes.
In `@app/subscription/core_templates.py`:
- Around line 7-10: The cached subscription_core_templates function currently
uses `@cached`() with no ttl so values never expire; add a modest TTL (e.g.,
ttl=300 or ttl=600) to the `@cached` decorator on subscription_core_templates to
ensure the cache auto-invalidates periodically as a safety net if NATS messages
to handle_core_template_message are missed or delayed, while keeping
get_core_template_values and the existing manual invalidation logic intact (use
the same decorator signature where subscription_core_templates is defined).
In `@app/subscription/default_templates.py`:
- Around line 18-22: The default template contains hardcoded region-specific DNS
entries under the key "proxy-server-nameserver" (values 178.22.122.100 and
78.157.42.100); update the template in app/subscription/default_templates.py to
use globally appropriate defaults (for example 1.1.1.1 and 8.8.8.8) or replace
these values with clearly documented region-specific placeholders and add a
comment/README note indicating they are Iran-specific so operators can override
them; locate the "nameserver" and "proxy-server-nameserver" entries in the
template and change the values or add the documentation accordingly.
In `@app/subscription/share.py`:
- Around line 75-76: Wrap the call to subscription_core_templates() in a
try/except to catch any exceptions, log the error (e.g., logging.exception or
processLogger.error) and set a safe fallback for core_templates (for example a
default templates list or an empty set) so that
_build_subscription_config(config_format, core_templates) still runs and returns
a usable conf; ensure you reference subscription_core_templates(),
core_templates and _build_subscription_config in the change and do not let the
original exception propagate to the HTTP handler.
- Around line 37-65: Replace the raw string template keys in
_build_subscription_config with module-level constants (e.g.,
USER_AGENT_TEMPLATE, GRPC_USER_AGENT_TEMPLATE, XRAY_SUBSCRIPTION_TEMPLATE,
SINGBOX_SUBSCRIPTION_TEMPLATE, CLASH_SUBSCRIPTION_TEMPLATE) and use those
constants when constructing common_kwargs and when passing template_*_content
into StandardLinks, ClashMetaConfiguration, SingBoxConfiguration,
XrayConfiguration, and OutlineConfiguration; also update the provider of
core_templates (subscription_core_templates) to populate and export the same
constants so both functions reference the single source of truth and avoid
KeyError from mistyped literal keys.
In `@app/subscription/singbox.py`:
- Around line 19-31: Wrap the template rendering and JSON parsing in
SingboxSubscription.__init__ in a try/except around the call to
json.loads(render_template_string(singbox_template_content)) so
template/Jinja/JSON errors are caught; on exception, fall back to parsing
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and assign to self.config (and optionally
log the error), keeping render_template_string, json.loads,
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and self.config as the referenced symbols
to locate the change.
In `@tests/api/test_core_template.py`:
- Around line 7-26: The test_core_template_create_and_get test creates a
template via create_core_template but never deletes it, risking test pollution;
modify the test to remove the created template in a teardown step (e.g., wrap
the create/assert logic in try/finally and call the API to delete the template
by created["id"] in the finally block) or move creation to a pytest fixture that
yields the created template and deletes it in the fixture teardown, ensuring
cleanup uses the same client/Authorization header used in client.get.
app/operation/core_template.py
Outdated
| async def _validate_template_content(self, template_type: CoreTemplateType, content: str) -> None: | ||
| try: | ||
| if template_type == CoreTemplateType.clash_subscription: | ||
| rendered = render_template_string( | ||
| content, | ||
| { | ||
| "conf": {"proxies": [], "proxy-groups": [], "rules": []}, | ||
| "proxy_remarks": [], | ||
| }, | ||
| ) | ||
| yaml.safe_load(rendered) | ||
| return | ||
|
|
||
| rendered = render_template_string(content) | ||
| parsed = json.loads(rendered) | ||
| if template_type in (CoreTemplateType.user_agent, CoreTemplateType.grpc_user_agent) and not isinstance( | ||
| parsed, dict | ||
| ): | ||
| raise ValueError("User-Agent template content must render to a JSON object") | ||
| except Exception as exc: | ||
| await self.raise_error(message=f"Invalid template content: {str(exc)}", code=400) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find render_template_string definition in app/templates/
find app/templates -type f -name "*.py" | head -20Repository: PasarGuard/panel
Length of output: 110
🏁 Script executed:
# Look for the render_template_string function definition
rg -n "def render_template_string" --type pyRepository: PasarGuard/panel
Length of output: 181
🏁 Script executed:
# Search for Jinja2 undefined mode configuration
rg -n "undefined|StrictUndefined|ChainableUndefined|DebugUndefined" --type py app/templates/Repository: PasarGuard/panel
Length of output: 42
🏁 Script executed:
cat -n app/templates/__init__.pyRepository: PasarGuard/panel
Length of output: 1067
🏁 Script executed:
sed -n '46,66p' app/operation/core_template.pyRepository: PasarGuard/panel
Length of output: 1056
JSON template validation skips context — valid templates using undefined variables will be silently rejected.
The Clash branch (lines 49–55) passes a mock context to render_template_string, ensuring template variables resolve correctly. The JSON branch (line 59) calls render_template_string(content) with no context, resulting in an empty dict. Since the Jinja2 Environment uses default undefined mode (app/templates/init.py:15), undefined variables render as empty strings. For custom Xray or SingBox templates using any undefined variable (e.g., {{ outbounds | tojson }}), this produces malformed JSON that fails json.loads, incorrectly rejecting valid templates.
Provide minimal stub contexts for each JSON type (e.g., an empty outbounds list for Xray/SingBox) — mirroring the Clash approach — or skip validation entirely for JSON types and validate raw JSON structure before rendering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/operation/core_template.py` around lines 46 - 66, The JSON validation
branch in _validate_template_content calls render_template_string(content) with
no context so undefined Jinja variables render to empty strings and json.loads
can fail; update the JSON branch in _validate_template_content to call
render_template_string with a minimal stub context matching the JSON template
kinds (e.g., provide keys like outbounds: [], inbounds: [], proxies: [],
proxy-groups: [], rules: [], proxy_remarks: [] as appropriate) before
json.loads, or alternatively skip rendering and validate raw JSON structure
first; ensure changes reference CoreTemplateType and keep the existing
parsed-type check (e.g., for CoreTemplateType.user_agent and
CoreTemplateType.grpc_user_agent) so templates still must render to a JSON
object.
🧩 Analysis chain
🌐 Web query:
Jinja2 default behavior undefined variables UndefinedError render empty string
💡 Result:
In Jinja2’s default configuration, missing names/attributes resolve to an instance of jinja2.Undefined:
{{ missing }}renders as an empty string ('') and is falsy.- But many other operations on it (math, string ops, etc.) will raise
jinja2.exceptions.UndefinedError.
This is the documented behavior ofUndefined. [1]
If you want an error instead of rendering '', switch to StrictUndefined:
from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined)(or in Flask: app.jinja_env.undefined = StrictUndefined). [1][2]
If you want to keep default undefined behavior but provide a fallback value in the template, use the default filter:
{{ missing | default('') }}(default returns the fallback when the value is undefined; it can also be configured to treat falsy values as needing the default via its second parameter). [3]
Sources:
[1] Jinja “API” docs – Undefined / StrictUndefined behavior and UndefinedError
[2] Stack Overflow – setting Flask to StrictUndefined
[3] Jinja “Template Designer Documentation” – default filter behavior
🏁 Script executed:
fd -type f -name "*.py" | xargs rg "def render_template_string" -A 20Repository: PasarGuard/panel
Length of output: 459
🏁 Script executed:
fd -type f -name "*.py" | xargs rg "Environment\(" -B 2 -A 10 | head -100Repository: PasarGuard/panel
Length of output: 1109
🏁 Script executed:
rg "from jinja2|import.*Jinja" --type pyRepository: PasarGuard/panel
Length of output: 42
🏁 Script executed:
rg "CoreTemplateType\.(xray|singbox|user_agent|grpc_user_agent)" -B 2 -A 2 | head -50Repository: PasarGuard/panel
Length of output: 1110
🏁 Script executed:
fd -type f -name "*.py" | xargs rg "def.*test.*template|def.*test.*validate" | head -20Repository: PasarGuard/panel
Length of output: 1992
🏁 Script executed:
rg "clash_subscription|user_agent.*template" -B 3 -A 3 | head -80Repository: PasarGuard/panel
Length of output: 5113
🏁 Script executed:
cat tests/api/test_core_template.pyRepository: PasarGuard/panel
Length of output: 2851
🏁 Script executed:
rg "DEFAULT_.*TEMPLATE|DEFAULT_XRAY|DEFAULT_SINGBOX|DEFAULT_USER_AGENT" -A 5Repository: PasarGuard/panel
Length of output: 12861
JSON template validation inconsistent with Clash—other types lack stub context for Jinja2 variables.
_validate_template_content renders Clash templates with mock context (lines 49–55) to properly resolve Jinja2 variables. For Xray, SingBox, and user-agent types (lines 59–64), render_template_string(content) is called with no context. If a custom template contains Jinja2 expressions (e.g., {{ proxies | tojson }}), undefined variables render as empty strings or raise UndefinedError, producing invalid JSON and incorrectly rejecting the template. The render infrastructure is present for all types, so templates should either all receive mock/stub context to be consistent with Clash, or validation should be relaxed to check raw JSON structure before rendering.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/operation/core_template.py` around lines 46 - 66, The JSON template
validation in _validate_template_content is inconsistent: only
CoreTemplateType.clash_subscription gets a stub Jinja2 context before rendering,
causing other types (e.g., Xray, SingBox, user_agent, grpc_user_agent) to fail
on templates that reference variables; update the render call(s) inside
_validate_template_content so all template types use an appropriate mock context
(e.g., the same {"conf": {"proxies": [], "proxy-groups": [], "rules": []},
"proxy_remarks": []} or a minimal common stub) when calling
render_template_string for non-clash types, then parse the rendered output
(yaml.safe_load or json.loads as appropriate) and keep the existing exception
handling and raise_error behavior.
| replacement = None | ||
| if db_template.is_default: | ||
| replacement = await get_first_template_by_type(db, template_type, exclude_id=db_template.id) | ||
|
|
||
| await remove_core_template(db, db_template) | ||
|
|
||
| if replacement is not None: | ||
| await set_default_template(db, replacement) |
There was a problem hiding this comment.
Delete and default-promotion are in separate transactions — orphaned state on failure.
remove_core_template (line 176) commits the deletion immediately. If set_default_template (line 179) subsequently fails (e.g. transient DB error), the type has no default template until a manual fix. Combine both operations in a single transaction: have remove_core_template only issue a db.delete() without committing, and perform the set_default_template logic in the same unit of work before the shared commit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/operation/core_template.py` around lines 172 - 179, The deletion of a
core template is being committed inside remove_core_template while the default
promotion happens afterwards in set_default_template, which can leave the type
without a default on failure; change the flow so both delete and promotion occur
in a single DB transaction: either modify remove_core_template to only call
db.delete() without committing and let the caller (the code around
get_first_template_by_type / set_default_template) perform the
transaction/commit, or implement a new atomic helper (e.g.,
remove_core_template_and_promote_default) that runs delete +
set_default_template logic inside one transactional unit and commits once both
succeed; update callers to use the new transactional behavior and ensure no
intermediate commit happens in remove_core_template.
Delete and default-promotion are in separate transactions — leaves type without a default on failure.
remove_core_template (line 176) calls db.commit() inside the CRUD function, so the deletion is permanently committed. If set_default_template (line 179) subsequently fails (e.g. transient DB error during its own db.commit()), the type is left with no default template and no automatic recovery path.
Refactor both steps into a single unit of work: have remove_core_template issue db.delete() without committing, then perform the default-promotion before the shared final commit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/operation/core_template.py` around lines 172 - 179, The deletion commits
separately causing a window where a failure in promotion leaves the type without
a default; change the flow so deletion and promotion occur in one transaction:
update remove_core_template to only call db.delete(db_template) (no db.commit())
or add a non-committing variant, then in the caller (the function handling
db_template removal) call get_first_template_by_type (if db_template.is_default)
and set_default_template as needed, and finally perform a single db.commit()
once both deletion and default promotion succeed; references:
remove_core_template, set_default_template, get_first_template_by_type,
db_template, replacement.
| if config_format == "outline": | ||
| return OutlineConfiguration() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine OutlineConfiguration class definition and its parent
rg -n 'class OutlineConfiguration' --type=py -A 15Repository: PasarGuard/panel
Length of output: 1118
🏁 Script executed:
# Check the share.py file for all config instantiations to compare
rg -n 'Configuration\(\)' app/subscription/share.pyRepository: PasarGuard/panel
Length of output: 100
🏁 Script executed:
# Examine the BaseSubscription class to understand template handling
rg -n 'class BaseSubscription' --type=py -A 20Repository: PasarGuard/panel
Length of output: 1592
🏁 Script executed:
# Look at all config instantiation patterns in the file
sed -n '40,80p' app/subscription/share.pyRepository: PasarGuard/panel
Length of output: 1608
🏁 Script executed:
# View the complete OutlineConfiguration class to check for super().__init__() call
cat app/subscription/outline.pyRepository: PasarGuard/panel
Length of output: 1487
🏁 Script executed:
# Check if OutlineConfiguration uses any BaseSubscription attributes like user_agent_list
rg -n 'self\.user_agent' app/subscription/outline.py
rg -n 'self\.proxy_remarks' app/subscription/outline.py
rg -n 'self\.grpc_user_agent' app/subscription/outline.pyRepository: PasarGuard/panel
Length of output: 42
🏁 Script executed:
# Check the full function signature and context of _build_subscription_config
sed -n '45,70p' app/subscription/share.py | cat -nRepository: PasarGuard/panel
Length of output: 1098
OutlineConfiguration doesn't call super().__init__() — this is a design issue.
OutlineConfiguration extends BaseSubscription but its __init__() never calls the parent constructor. While it's instantiated bare without template parameters in share.py line 59 (unlike all other config types), the actual problem is deeper: OutlineConfiguration doesn't initialize BaseSubscription at all. This creates misleading inheritance — either OutlineConfiguration should not extend BaseSubscription, or it should call super().__init__(user_agent_template_content, grpc_user_agent_template_content) and handle templates properly. Currently, it works only because OutlineConfiguration doesn't use any parent class attributes, but this is fragile design.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/subscription/share.py` around lines 58 - 59, OutlineConfiguration
currently subclasses BaseSubscription but its __init__ does not call
super().__init__, leaving the parent uninitialized; either remove the
inheritance or update OutlineConfiguration.__init__ to call
super().__init__(user_agent_template_content, grpc_user_agent_template_content)
and properly store/handle those template args, and update the instantiation site
(the branch that returns OutlineConfiguration()) to pass the needed template
content variables so BaseSubscription is initialized correctly; refer to the
OutlineConfiguration class and BaseSubscription.__init__ for where to add the
super call and to the place that constructs OutlineConfiguration to supply the
two template parameters.
| def render_template_string(template_content: str, context: Union[dict, None] = None) -> str: | ||
| return env.from_string(template_content).render(context or {}) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, view the actual app/templates/__init__.py file to see the env setup and function definition
cat -n app/templates/__init__.pyRepository: PasarGuard/panel
Length of output: 1067
🏁 Script executed:
# Search for all calls to render_template_string to understand usage patterns
rg -n 'render_template_string' --type=py -B 2 -A 2Repository: PasarGuard/panel
Length of output: 4534
🏁 Script executed:
# Check the CoreTemplate model definition to understand data flow
rg -n 'class CoreTemplate' --type=py -A 15Repository: PasarGuard/panel
Length of output: 6503
🏁 Script executed:
# Look at the admin API endpoint that modifies template content
rg -n 'core_template' app/routers/ --type=py -B 2 -A 5Repository: PasarGuard/panel
Length of output: 6574
🏁 Script executed:
# Check the check_sudo_admin implementation
rg -n 'def check_sudo_admin' --type=py -A 10Repository: PasarGuard/panel
Length of output: 996
🏁 Script executed:
# Search for DEFAULT_*_TEMPLATE definitions and usage
rg -n 'DEFAULT_.*TEMPLATE' --type=py -B 2 -A 2Repository: PasarGuard/panel
Length of output: 10574
🏁 Script executed:
# Check if CoreTemplate content can be modified outside the router API
rg -n 'CoreTemplate' --type=py | grep -i 'update\|modify\|content.*='Repository: PasarGuard/panel
Length of output: 793
Server-Side Template Injection (SSTI) risk with unsandboxed Jinja2 environment.
The SSTI vulnerability is valid: env.from_string(template_content) renders database-sourced template content through an unsandboxed Jinja2 Environment, allowing arbitrary Python code execution via template payloads (e.g., {{ ''.__class__.__mro__... }}).
However, note that CoreTemplate content can only be modified by sudo admins (all create/modify endpoints require check_sudo_admin). The risk exists but is narrower in scope than general admin escalation—it requires either a compromised sudo admin account or a malicious insider with sudo privileges.
Consider using a SandboxedEnvironment for defense-in-depth:
🔒 Proposed fix using SandboxedEnvironment
+from jinja2.sandbox import SandboxedEnvironment
+
+sandboxed_env = SandboxedEnvironment()
+sandboxed_env.filters.update(CUSTOM_FILTERS)
+sandboxed_env.globals["now"] = lambda: dt.now(tz.utc)
+
+
def render_template_string(template_content: str, context: Union[dict, None] = None) -> str:
- return env.from_string(template_content).render(context or {})
+ return sandboxed_env.from_string(template_content).render(context or {})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def render_template_string(template_content: str, context: Union[dict, None] = None) -> str: | |
| return env.from_string(template_content).render(context or {}) | |
| from jinja2.sandbox import SandboxedEnvironment | |
| sandboxed_env = SandboxedEnvironment() | |
| sandboxed_env.filters.update(CUSTOM_FILTERS) | |
| sandboxed_env.globals["now"] = lambda: dt.now(tz.utc) | |
| def render_template_string(template_content: str, context: Union[dict, None] = None) -> str: | |
| return sandboxed_env.from_string(template_content).render(context or {}) |
🧰 Tools
🪛 GitHub Check: CodeQL
[failure] 25-25: Server Side Template Injection
This template construction depends on a user-provided value.
This template construction depends on a user-provided value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/templates/__init__.py` around lines 24 - 25, The render_template_string
function currently calls env.from_string(template_content) which renders
untrusted DB-sourced templates with an unsandboxed Jinja2 Environment; replace
this with a SandboxedEnvironment-based render: create or use
jinja2.sandbox.SandboxedEnvironment to parse and render template_content
(preserving the same context fallback logic), so that render_template_string
uses SandboxedEnvironment.from_string(...).render(context or {}) instead of
env.from_string(...).render(...), ensuring the template execution is sandboxed
for defense-in-depth while keeping the function signature and context handling
intact.
c80013a to
9cc26bd
Compare
Summary by CodeRabbit
Release Notes
New Features
Chores